S03-09 核心类-Java比较器
[TOC]
概述
在 Java 中,当我们需要对一组对象进行排序时(比如一个存储了自定义 User 对象的 List),Java 并不知道应该按照什么规则去排——是按年龄、按名字,还是按入职时间?
为了解决这个问题,Java 提供了两个核心接口:Comparable 和 Comparator。它们被称为 Java 的比较器。
Comparable 接口
Comparable(自然排序 / 内部比较器):
在 Java 中,如果你有一组自定义对象(比如用户、商品或学生),想直接使用 Collections.sort() 或 Arrays.sort() 对它们进行排序,Java 会面临一个问题:它不知道应该按照哪个属性来排。
这就是 Comparable 接口的核心作用。它用于为类定义自然排序(Natural Ordering)规则。换句话说,实现这个接口就等于告诉 Java:“我们这个类的对象,默认就应该按照这种规则来比大小。”
compareTo()
核心方法::compareTo
Comparable<T> 接口非常简单,内部只有一个抽象方法需要你实现:
public interface Comparable<T>{
int compareTo(T o);
}这个方法的返回值是一个整数,它的核心逻辑如下(假设我们要比较 A.compareTo(B)):
- 返回负整数(通常是
-1):表示A小于B(A应该排在B前面)。 - 返回零(
0):表示A等于B。 - 返回正整数(通常是
1):表示A大于B(A应该排在B后面)。
实战案例
实战演练:让 :User 类支持按年龄排序
我们来看一个具体的例子。假设有一个 User 类,我们希望默认按照年龄从小到大升序排列:
// 1. 实现 Comparable 接口
public class User implements Comparable<User> {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
// 2. 实现 Comparable 接口的抽象方法 compareTo()
@Override
public int compareTo(User other) {
// 升序排列:当前对象年龄减去对比对象年龄
return Integer.compare(this.age, other.age);
}
@Override
public String toString() {
return name + "(" + age + ")";
}
}为什么推荐 Integer.compare():
避坑指南: 过去很多人喜欢直接写
return this.age - other.age;。这种写法虽然简洁,但在处理大整数时(比如this.age是正数,other.age是负数)可能会导致整数溢出,从而产生完全相反的排序结果。使用包装类的.compare()方法是最安全的选择。
现在我们可以直接对 User 列表排序了:
List<User> users = new ArrayList<>();
users.add(new User("张三", 25));
users.add(new User("李四", 20));
users.add(new User("王五", 30));
Collections.sort(users); // 会自动调用 User 类中的 compareTo
System.out.println(users); // 输出: [李四(20), 张三(25), 王五(30)]核心规则
必须遵守的“军规”(Contract):
实现 Comparable 时,有几个虽然编译器不强制、但写代码必须遵守的原则,否则在使用 TreeSet 或 TreeMap 等集合时会发生不可预知的 Bug:
对称性:如果
A.compareTo(B) > 0,那么B.compareTo(A)必须小于 0。传递性:如果
A > B且B > C,那么必须有A > C。与
equals保持一致(强烈建议):如果A.compareTo(B) == 0,那么A.equals(B)最好也返回true。因为诸如TreeSet这类集合在去重时,是用compareTo来判断元素是否重复的,而不是equals。
Comparator 接口
Comparator(定制排序 / 外部比较器):
如果说 Comparable 是给类穿上了一件“出厂自带”的固定衣服,那么 Comparator 就是更衣室里的百变战袍。
实战痛点:
在实际开发中,我们经常遇到两种痛点:
源码改不动:比如你想对
String或第三方 Jar 包里的类排序,你没法修改人家的源码去实现Comparable接口。规则总在变:比如商品列表,用户一会儿想按“价格从低到高”排,一会儿想按“销量从高到低”排。一个类只能实现一个
Comparable方法,根本应付不来。
这时候,就需要 Comparator 闪亮登场了。它就像一个独立的裁判(工具类),不需要修改类本身,只需要你在排序时把它作为参数传进去即可。
compare()
核心方法::compare()
Comparator<T> 是一个函数式接口(Functional Interface),它的核心方法是:
public interface Comparator<T>{
int compare(T o1, T o2);
}这个方法的返回值是一个整数,它的核心逻辑如下(假设我们要比较 compare(o1, o2)):
- 返回负整数(通常是
-1):表示o1小于o2(o1应该排在o2前面)。 - 返回零(
0):表示o1等于o2。 - 返回正整数(通常是
1):表示o1大于o2(o1应该排在o2后面)。
传统写法
Java 8 之前如果你想自定义排序规则,主要有两种流水线式的写法:匿名内部类和独立实现类。同时,排序只能依靠 Collections.sort() 这个静态方法。
写法一:匿名内部类
匿名内部类(“临时”写法):
如果你只需要在某一个地方临时对列表排个序,最常见的就是直接在 Collections.sort() 里现场“手写”一个 Comparator 的匿名内部类。
假设我们依然有一个 Student 类(有 name 和 score 属性),在 Java 7 或更早的版本里,按分数升序排列必须这样写:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
// 假设这是当年的一段业务代码
List<Student> students = new ArrayList<Student>();
students.add(new Student("张三", 85));
students.add(new Student("李四", 92));
// Java 8 之前没有 list.sort(),必须用 Collections.sort()
Collections.sort(students, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
// 经典的包装类安全比较
return Integer.compare(o1.getScore(), o2.getScore());
}
});匿名内部类写法的痛点:
核心逻辑其实只有 Integer.compare(...) 这一行,但为了这一行,你必须硬生生附带上 new Comparator<Student>()、@Override、public int compare... 等整整 5 行外壳。这种为了传递一个“动作”(比较逻辑),不得不包裹一个“对象”(匿名内部类)的做法,正是当年 Java 被诟病“罗嗦”的主要原因。
写法二:独立实现类
独立实现类(用于复用):
如果某一种排序规则在好几个地方都要用(比如“按学生分数降序排列”),匿名内部类就不合适了。那时我们会专门写一个类去实现 Comparator 接口:
// 1. 专门定义一个比较器类
public class StudentScoreDescComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
// 降序:o2 放前面
return Integer.compare(o2.getScore(), o1.getScore());
}
}
// 2. 在业务代码中使用
List<Student> students = new ArrayList<Student>();
// ... 添加数据 ...
// 传入比较器的实例
Collections.sort(students, new StudentScoreDescComparator());灾难现场:多条件组合排序
灾难现场:多条件组合排序:
在 Java 8 之前,没有像 .thenComparing() 这样优雅的链式调用。如果你想实现“先按分数降序,分数相同再按名字字母升序”这种多条件排序,所有的逻辑都必须揉在一个 compare 方法里,形成复杂的嵌套:
Collections.sort(students, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
// 1. 先比分数(降序)
int scoreResult = Integer.compare(o2.getScore(), o1.getScore());
// 2. 如果分数一样,再比名字
if (scoreResult == 0) {
if (o1.getName() == null && o2.getName() == null) return 0;
if (o1.getName() == null) return -1;
if (o2.getName() == null) return 1;
return o1.getName().compareTo(o2.getName());
}
return scoreResult;
}
});这种代码不仅阅读起来容易眼花,一旦排序条件增加到三四个,内部的 if-else 就会像套娃一样可怕,极易翻车。
传统写法的局限
回顾 Java 8 之前的 Comparator,主要有以下三个硬伤:
语法大国,废话太多:为了传一个函数,必须写一个类。
集合自身无法排序:
List接口当时没有sort()方法,必须通过外部工具类Collections.sort(list, comparator)来间接完成。缺乏组合拳能力:没有提供诸如
reversed()、thenComparing()、nullsFirst()等工具方法,所有稍微复杂的复合逻辑、逆序逻辑、判空逻辑,全部需要程序员纯手工用if-else在compare内部垒砖。
这也正是为什么 2014 年 Java 8 推出 Lambda 和静态工厂方法时,整个 Java 社区都欢呼雀跃的原因——它让原本沉重的代码,终于变得像现代编程语言一样轻盈了。
现代写法(推荐)
现代 Java(Java 8+)的优雅玩法:
在过去,写 Comparator 必须写一堆臃肿的匿名内部类。现代 Java 引入了 Lambda 表达式和强大的静态工厂方法,让排序变得像写英语句子一样流畅。
我们用一个 Product(商品)类来演示:
public class Product {
private String name;
private double price;
private int sales;
// 构造方法、Getters 和 toString 略
}基础用法:按价格升序:
使用
Comparator.comparing()配合方法引用,可以直接提取属性进行比较:javaList<Product> products = getProductList(); // 假设获取到了商品列表 // 按价格升序排列 products.sort(Comparator.comparing(Product::getPrice));降序排列:按销量从高到低:
想换成降序?不需要重写逻辑,直接在后面连缀一个
.reversed()即可:java// 按销量降序排列(从大到小) products.sort(Comparator.comparing(Product::getSales).reversed());多条件组合排序:先按价格升序,价格相同按销量降序:
这是电商系统里最常见的组合排序需求。使用
thenComparing就能像搭积木一样组合规则:javaproducts.sort( Comparator.comparing(Product::getPrice) // 1. 先按价格升序 .thenComparing(Comparator.comparing(Product::getSales).reversed()) // 2. 价格相同,按销量降序 );
null 值保护
警惕 NullPointerException(空指针异常):
如果你的商品列表里,某个商品的销量或者价格是 null,直接排序会瞬间抛出 NullPointerException。
Comparator 贴心地准备了 nullsFirst 和 nullsLast 方法,来决定把 null 值排在最前还是最后:
// 把价格为 null 的商品排在最前面,其余的按价格升序
products.sort(
Comparator.comparing(Product::getPrice, Comparator.nullsFirst(Double::compare))
);原始类型优化
原始类型(Primitive)的优化:
如果你要比较的是 int、long 或 double 这样的基本数据类型,为了避免频繁自动装箱(Auto-boxing) 带来的性能损耗,建议使用专有的方法:
Comparator.comparingInt()Comparator.comparingLong()Comparator.comparingDouble()java// 性能更好的写法 products.sort(Comparator.comparingInt(Product::getSales));
Comparable vs Comparator
在 Java 中还有另一个长得很像的接口叫 Comparator,它们经常被拿来对比:
| 特性 | Comparable (内部比较器) | Comparator (外部比较器) |
|---|---|---|
| 包位置 | java.lang.Comparable | java.util.Comparator |
| 核心方法 | compareTo(T o) | compare(T o1, T o2) |
| 对类结构的影响 | 必须修改原类的代码,让原类实现该接口。 | 无需修改原类,可以独立定义一个比较规则类。 |
| 灵活度 | 一旦定义,通常是“一劳永逸”的默认/自然规则。 | 非常灵活,可以根据不同场景传入不同的比较规则。 |
| 使用场景 | 类的属性比较单一、排序规则固定的情况。 | 原类代码无法修改(如第三方库),或者需要多种排序规则(如今天按价格排,明天按销量排)。 |